﻿using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System;
using System.Collections.Generic;

namespace Sustainalytics
{
    public interface ITimestamped
    {
        object Value { get; }
        DateTime? Timestamp { get; }
    }

    public class Timestamped<T> : ITimestamped, IComparable<Timestamped<T>>, IComparable
    {
        public T Value { get; set; }

        public DateTime? Timestamp { get; set; }

        public static implicit operator T(Timestamped<T> timestamped)
            => timestamped != null ? timestamped.Value : default(T);

        public static implicit operator Timestamped<T>(T value) => value.Timestamped();

        public override string ToString() => Value.ToString();

        public Timestamped<U> Bind<U>(Func<T, U> func) => func(Value).Timestamped(Timestamp);

        object ITimestamped.Value => Value;

        public override int GetHashCode() => Value.GetHashCode() ^ Timestamp.GetHashCode();

        public override bool Equals(object obj)
        {
            if (obj is null)
                return false;

            if (!(obj is Timestamped<T> other))
                return false;

            return Value.Equals(other.Value) && Timestamp.Equals(other.Timestamp);
        }

        public int CompareTo(Timestamped<T> other)
        {
            if (other is null)
                return 1;

            if (Equals(other))
                return 0;

            if (Value.Equals(other.Value))
                return Nullable.Compare(Timestamp, other.Timestamp);

            return Comparer<T>.Default.Compare(Value, other.Value);
        }

        public int CompareTo(object obj) => CompareTo(obj as Timestamped<T>);

        public static bool operator ==(Timestamped<T> timestamped1, Timestamped<T> timestamped2)
            => timestamped1 is null ? timestamped2 is null : timestamped1.Equals(timestamped2);

        public static bool operator !=(Timestamped<T> timestamped1, Timestamped<T> timestamped2)
            => !(timestamped1 == timestamped2);

        public static bool operator <(Timestamped<T> timestamped1, Timestamped<T> timestamped2)
            => !(timestamped1 is null) && timestamped1.CompareTo(timestamped2) < 0;

        public static bool operator >(Timestamped<T> timestamped1, Timestamped<T> timestamped2)
            => !(timestamped1 is null) && timestamped1.CompareTo(timestamped2) > 0;

        public static bool operator <=(Timestamped<T> timestamped1, Timestamped<T> timestamped2)
            => timestamped1 < timestamped2 || timestamped1 == timestamped2;

        public static bool operator >=(Timestamped<T> timestamped1, Timestamped<T> timestamped2)
            => timestamped1 > timestamped2 || timestamped1 == timestamped2;
    }

    public class TimestampedFloat : ITimestamped
    {
        [BsonRepresentation(BsonType.Double, AllowTruncation = true)]
        public float Value { get; set; }

        public DateTime? Timestamp { get; set; }

        public override string ToString() => Value.ToString();

        public Timestamped<U> Bind<U>(Func<float, U> func) => func(Value).Timestamped(Timestamp);

        object ITimestamped.Value => Value;

        public static implicit operator float(TimestampedFloat timestamped) => timestamped.Value;

        public static implicit operator TimestampedFloat(float value) => new TimestampedFloat { Value = value };

        public override int GetHashCode() => Value.GetHashCode() ^ Timestamp.GetHashCode();

        public override bool Equals(object obj)
        {
            if (obj is null)
                return false;

            if (!(obj is Timestamped<float> other))
                return false;

            return Value.Equals(other.Value) && Timestamp.Equals(other.Timestamp);
        }

        public int CompareTo(TimestampedFloat other)
        {
            if (other is null)
                return 1;

            if (Equals(other))
                return 0;

            if (Value.Equals(other.Value))
                return Nullable.Compare(Timestamp, other.Timestamp);

            return Comparer<float>.Default.Compare(Value, other.Value);
        }

        public int CompareTo(object obj) => CompareTo(obj as TimestampedFloat);

        public static bool operator ==(TimestampedFloat timestamped1, TimestampedFloat timestamped2)
            => timestamped1 is null ? timestamped2 is null : timestamped1.Equals(timestamped2);

        public static bool operator !=(TimestampedFloat timestamped1, TimestampedFloat timestamped2)
            => !(timestamped1 == timestamped2);

        public static bool operator <(TimestampedFloat timestamped1, TimestampedFloat timestamped2)
            => !(timestamped1 is null) && timestamped1.CompareTo(timestamped2) < 0;

        public static bool operator >(TimestampedFloat timestamped1, TimestampedFloat timestamped2)
            => !(timestamped1 is null) && timestamped1.CompareTo(timestamped2) > 0;

        public static bool operator <=(TimestampedFloat timestamped1, TimestampedFloat timestamped2)
            => timestamped1 < timestamped2 || timestamped1 == timestamped2;

        public static bool operator >=(TimestampedFloat timestamped1, TimestampedFloat timestamped2)
            => timestamped1 > timestamped2 || timestamped1 == timestamped2;
    }

    public static class TimestampedExtensions
    {
        public static Timestamped<T> Timestamped<T>(this T value, DateTime? timestamp = null)
            => new Timestamped<T> { Value = value, Timestamp = timestamp };

        public static Timestamped<T> GetValueOrDefault<T>(this Timestamped<T?> timestamped, T defaultValue = default(T)) where T : struct
            => timestamped?.Bind<T>(x => x.GetValueOrDefault(defaultValue));
    }
}
